package com.eairlv.route.client.core;

import cn.hutool.cache.Cache;
import cn.hutool.cache.CacheUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.eairlv.route.client.annotation.RouteKey;
import com.eairlv.route.client.annotation.RouteParameter;
import com.eairlv.route.client.configuration.RouteProperties;
import com.eairlv.route.client.entity.AuthenticationProperties;
import com.eairlv.route.client.entity.Result;
import com.eairlv.route.client.entity.RouteForm;
import com.eairlv.route.client.entity.RouteHandlerForm;
import com.eairlv.route.client.service.RouteLoadService;
import com.eairlv.route.client.utils.LogUtil;
import feign.InvocationHandlerFactory;
import feign.Target;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static feign.Util.checkNotNull;

/**
 * @author eairlv
 * @description
 * @date 11:21 2019/12/9
 */
@Slf4j
public class RouteInvocationHandler implements InvocationHandler {

    public static final String NO_AES_KEY = "no aesKey";

    public static final int RANDOM_NUMBER_LENGTH = 19;

    public static ThreadLocal<String> route_header = new ThreadLocal<>();

    public static ThreadLocal<String> route_before = new ThreadLocal<>();
    public static ThreadLocal<String> route_after = new ThreadLocal<>();

    private Map<String, Cache<String, String>> map = new HashMap<>();

    private ExpressionParser parser = new SpelExpressionParser();

    private final Target target;
    private final Map<Method, InvocationHandlerFactory.MethodHandler> dispatch;

    private RouteLoadService routeLoadService;
    private RouteProperties routeProperties;

    RouteInvocationHandler(Target target, Map<Method, InvocationHandlerFactory.MethodHandler> dispatch, RouteLoadService routeLoadService) {
        this.target = checkNotNull(target, "target");
        this.dispatch = checkNotNull(dispatch, "dispatch for %s", target);
        this.routeLoadService = routeLoadService;
        this.routeProperties = routeLoadService.getRouteClientService().getRouteProperties();
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if ("equals".equals(method.getName())) {
            try {
                Object otherHandler = args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
                return equals(otherHandler);
            } catch (IllegalArgumentException e) {
                return false;
            }
        } else if ("hashCode".equals(method.getName())) {
            return hashCode();
        } else if ("toString".equals(method.getName())) {
            return toString();
        }
        return route(method, args);
    }

    /**
     * 路由处理器
     * @param method
     * @param args
     * @return
     */
    private Object route(Method method, Object[] args) throws Throwable{
        // 默认返回值
        Object invoke = Result.failMessage(method.getReturnType());

        // 装载路由配置
        RouteKey routeConfig = routeConfig(method);
        if (routeConfig != null){
            if (StrUtil.isNotBlank(routeConfig.address())){
                // 直接替换路由信息
                String token = getToken(routeConfig.aesKey(), routeConfig.address(), routeConfig.expire());
                if (token == null){
                    return Result.failMessage("get token by address error of key: " + routeConfig.aesKey(), method.getReturnType());
                }
                setRoute(target.url(), routeConfig.address(), target.name(), token);
            } else if(StrUtil.isNotBlank(routeConfig.session())) {
                // 直接通过session获取路由信息
                if (!routeConfig.session().equals(routeProperties.getDefaultSession())){
                    AuthenticationProperties route = routeLoadService.getRouteBySession(routeConfig.session());
                    if (route == null){
                        return Result.failMessage("get token by session error of session: " + routeConfig.session(), method.getReturnType());
                    }
                    // 替换路由信息
                    String token = getToken(route.getAesKey(), route.getSession(), route.getExpire());
                    if (token == null){
                        return Result.failMessage("get token by session error of key: " + route.getAesKey(), method.getReturnType());
                    }
                    setRoute(target.url(), route.getAddress(), target.name(), token);
                }
            } else {
                // 获取路由spel表达式
                String routeKey = routeConfig.value();
                if (StrUtil.isBlank(routeKey)){
                    return Result.failMessage("no route key", method.getReturnType());
                }

                // 获取feign方法参数名
                String[] params = getParameterNames(method, args);
                if (params.length == 0){
                    return Result.failMessage("no route parameter", method.getReturnType());
                }

                try {
                    // 获取路由spel表达式结果
                    EvaluationContext context = new StandardEvaluationContext();
                    for (int len = 0; len < params.length; len++) {
                        context.setVariable(params[len], args[len]);
                    }
                    Expression expression = parser.parseExpression(routeKey);
                    Object key = expression.getValue(context, Object.class);
                    if (key == null){
                        return Result.failMessage("route key： " + routeKey + " find the key is null", method.getReturnType());
                    }

                    // 获取路由信息
                    AuthenticationProperties route = routeLoadService.getRoute(key, routeProperties.getDefaultType(), method, args, target);
                    if (route == null){
                        RouteHandlerForm routeHandlerForm = new RouteHandlerForm();
                        routeHandlerForm.setKey(key);
                        routeHandlerForm.setType(routeProperties.getDefaultType());
                        routeHandlerForm.setSession(routeProperties.getDefaultSession());
                        if (routeLoadService.setRoute(routeHandlerForm, method, args, target)){
                            route = routeLoadService.getRoute(key, routeProperties.getDefaultType(), method, args, target);
                        } else {
                            return Result.failMessage("key：" + JSON.toJSONString(key) + " route create failed", method.getReturnType());
                        }
                    }
                    if (!route.getSession().equals(routeProperties.getDefaultSession())){
                        // 替换路由信息
                        String token = getToken(route.getAesKey(), route.getSession(), route.getExpire());
                        if (token == null){
                            return Result.failMessage("get token by routeKey error of key: " + route.getAesKey(), method.getReturnType());
                        }
                        setRoute(target.url(), route.getAddress(), target.name(), token);
                    }
                } catch (Exception e) {
                    log.warn("route build error: {}", LogUtil.getExceptionMessage(e));
                    return invoke;
                }
            }
        }
        try {
            // 路由请求
            invoke = dispatch.get(method).invoke(args);
        } catch (Exception e){
            log.warn("route request error: {}", LogUtil.getExceptionMessage(e));
        }
        // 清理路由信息
        clearRoute();
        log.info("#路由结果：{}", JSON.toJSONString(invoke));
        return invoke;
    }

    /**
     * 设置路由
     * @param url
     * @param address
     * @param name
     * @param token
     */
    private void setRoute(String url, String address, String name, String token){
        route_before.set(url);
        route_after.set(address + "/" + name);
        route_header.set(token);
    }

    /**
     * 清理路由
     */
    private void clearRoute(){
        RouteInvocationHandler.route_before.remove();
        RouteInvocationHandler.route_after.remove();
        RouteInvocationHandler.route_header.remove();
    }

    /**
     * 通过session获取token
     * @param aesKey
     * @return
     */
    public String getToken(String aesKey, String session, Long expire) {
        String token = getTokenCache(session, expire);
        if (token == null){
            if (StrUtil.isNotBlank(aesKey)){
                try {
                    byte[] raw = aesKey.getBytes("utf-8");
                    SecretKeySpec secretKeySpec = new SecretKeySpec(raw, "AES");
                    Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
                    cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
                    byte[] encrypted = cipher.doFinal((RandomUtil.randomNumbers(RANDOM_NUMBER_LENGTH) + System.currentTimeMillis()).getBytes("utf-8"));
                    String newToken = URLEncoder.encode(new Base64().encodeToString(encrypted), "utf-8");
                    map.get(session).put(session, newToken);
                    return newToken;
                } catch (Exception e){
                    log.warn("session aes to token error: {}", LogUtil.getExceptionMessage(e));
                }
            } else {
                // 如果不设置aesKey，生成token内容为"no aesKey"，并且再请求发送时不会将此header放入请求中
                return NO_AES_KEY;
            }
        }
        return token;
    }

    /**
     * 获取token缓存
     * session:token
     * @param session
     * @param expire
     * @return
     */
    private String getTokenCache(String session, Long expire){
        Cache<String, String> cache = map.get(session);
        if (cache != null){
            return cache.get(session);
        } else {
            map.put(session, CacheUtil.newTimedCache(expire));
        }
        return getTokenCache(session, expire);
    }

    /**
     * 获取路由注解
     * @param method
     * @return
     */
    private RouteKey routeConfig(Method method){
        RouteKey annotation = method.getAnnotation(RouteKey.class);
        if (annotation != null){
            return annotation;
        }
        return null;
    }



    /**
     * 获取feign方法参数名
     * @param method
     * @param args
     * @return
     */
    private String[] getParameterNames(Method method, Object[] args) {
        List<String> parameters = new ArrayList<>();
        Annotation[][] parameterAnnotations = method.getParameterAnnotations();
        for (int i = 0; i < parameterAnnotations.length; i++) {
            Annotation[] annotations = parameterAnnotations[i];
            if (annotations != null) {
                for (Annotation annotation : annotations) {
                    if (annotation instanceof RouteParameter) {
                        parameters.add(((RouteParameter) annotation).value());
                    }
                }
            }
        }
        if (parameters.isEmpty()){
            for (int i = 0; i < args.length; i++) {
                parameters.add(toLowerCaseFirstOne(args[i].getClass().getSimpleName()));
            }
        }
        return parameters.toArray(new String[0]);
    }

    /**
     * 首字母转小写
     * @param s
     * @return
     */
    private String toLowerCaseFirstOne(String s){
        if(Character.isLowerCase(s.charAt(0)))
            return s;
        else
            return (new StringBuilder()).append(Character.toLowerCase(s.charAt(0))).append(s.substring(1)).toString();
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof RouteInvocationHandler) {
            RouteInvocationHandler other = (RouteInvocationHandler) obj;
            return target.equals(other.target);
        }
        return false;
    }

    @Override
    public int hashCode() {
        return target.hashCode();
    }

    @Override
    public String toString() {
        return target.toString();
    }

}
